package com.uxsino.simo.utils;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.function.Consumer;

import org.apache.commons.lang3.StringUtils;
import org.w3c.dom.DOMException;

import com.uxsino.commons.utils.config.ConfigProp;
import com.uxsino.commons.utils.config.PropElement;

public class ConfigPropLoader {
    private ConfigLoadingContext lctxt;

    public ConfigPropLoader(ConfigLoadingContext lctxt) {
        this.lctxt = lctxt;
    }

    public void loadProperties(Object config, PropElement element)
        throws IllegalAccessException, IllegalArgumentException, InvocationTargetException {
        PropSetter setter = new PropSetter(config, element);

        doWithMethods(config.getClass(), setter::doWithMethod);
        doWithFields(config.getClass(), setter::doWithField);
    }

    private void doWithMethods(Class<?> clazz, Consumer<Method> mc)
        throws IllegalArgumentException, IllegalAccessException, InvocationTargetException {
        // Keep backing up the inheritance hierarchy.
        Method[] methods = clazz.getDeclaredMethods();
        for (Method method : methods) {
            mc.accept(method);
        }
        if (clazz.getSuperclass() != null) {
            doWithMethods(clazz.getSuperclass(), mc);
        }
        for (Class<?> superIfc : clazz.getInterfaces()) {
            doWithMethods(superIfc, mc);
        }
    }

    private void doWithFields(Class<?> clazz, Consumer<Field> mc) {
        // Keep backing up the inheritance hierarchy.
        Field[] fields = clazz.getDeclaredFields();
        for (Field field : fields) {
            mc.accept(field);
        }
        if (clazz.getSuperclass() != null) {
            doWithFields(clazz.getSuperclass(), mc);
        } else if (clazz.isInterface()) {
            for (Class<?> superIfc : clazz.getInterfaces()) {
                doWithFields(superIfc, mc);
            }
        }
    }

    private final class PropSetter {

        private PropElement element;

        private Object target;

        public PropSetter(Object target, PropElement element) {
            this.element = element;
            this.target = target;
        }

        private String getProp(ConfigProp prop) {

            String sElement = null;
            if (prop.subElement()) {
                PropElement el = element.getFirstElement(prop.name());
                if (el != null)
                    sElement = el.getText();
            }

            String s = element.getProp(prop.name());

            if (StringUtils.isBlank(s)) {
                return s;
            }

            if (StringUtils.isNotEmpty(sElement)) {
                if (StringUtils.isNotEmpty(s)) {
                    lctxt.error(element.getSourceLocation(), "duplicate {} in attribute and as element. use attibute",
                        prop.name());
                } else {
                    s = sElement;
                }
            }
            if (prop.trim()) {
                s = s.trim();
            }
            switch (prop.letterCase()) {
            case UPPER:
                s = s.toUpperCase();
                break;
            case LOWER:
                s = s.toLowerCase();
                break;
            case NONE:
            }
            return s;
        }

        private Object parseValue(Class<?> valueType, String s) {
            if (valueType == String.class) {
                return s;
            } else if (valueType.equals(Integer.class) || valueType.equals(Integer.TYPE)) {
                try {
                    return Integer.parseInt(s);
                } catch (NumberFormatException e) {
                    lctxt.error(element.getSourceLocation(), "cannot parse \'{}\' into integer", s);
                    throw e;
                }
            } else if (valueType.equals(Double.class) || valueType.equals(Double.TYPE)) {
                try {
                    return Double.parseDouble(s);
                } catch (NumberFormatException e) {
                    lctxt.error(element.getSourceLocation(), "cannot parse \'{}\' into double", s);
                    throw e;
                }
            } else if (valueType.isEnum()) {
                return parseEnum(valueType, s);
            }

            throw new IllegalArgumentException("unknown type: " + valueType.getName());
        }

        @SuppressWarnings("unchecked")
        private Object parseEnum(Class<?> valueType, String s) {
            @SuppressWarnings("rawtypes")
            Class<? extends Enum> etype = valueType.asSubclass(Enum.class);
            return Enum.valueOf(etype, s);
        }

        public void doWithMethod(Method method) {

            ConfigProp prop = method.getAnnotation(ConfigProp.class);
            if (prop == null)
                return;
            // load all attributes into a map
            if (prop.name().compareTo("*") == 0) {

                for (Map.Entry<String, String> entry : element.getProps().entrySet()) {
                    try {
                        method.invoke(target, entry.getKey(), entry.getValue());
                    } catch (IllegalArgumentException | InvocationTargetException | DOMException e) {
                        lctxt.error(
                            element.getSourceLocation(), "error loading attributes into "
                                    + method.getDeclaringClass().getSimpleName() + "." + method.getName() + ". " + "",
                            e);

                    } catch (IllegalAccessException ae) {
                        lctxt.error(element.getSourceLocation(), "illegal access? should not happen. {}", ae);
                    }
                }
                return;
            }

            String s = getProp(prop);
            if (StringUtils.isNoneEmpty(s)) {
                if (method.getParameterCount() == 1) {
                    Class<?> t = method.getParameterTypes()[0];
                    try {
                        method.setAccessible(true);
                        method.invoke(target, parseValue(t, s));
                    } catch (InvocationTargetException | IllegalArgumentException e) {
                        lctxt.error(element.getSourceLocation(), "error setting property {}.{}. value:{}. {}",
                            target.getClass().getSimpleName(), prop.name(), s, e);
                    } catch (IllegalAccessException ae) {
                        lctxt.error(element.getSourceLocation(), "illegal access? should not happen. {}", ae);
                    }
                }
            } else if (prop.required()) {
                String src = (String) element.getSourceName();

                if (src == null)
                    src = "";
                else
                    src = src + "[" + element.getSourceLocation() + "]";
                throw new IllegalArgumentException(
                    src + " " + prop.name() + " is required  for " + target.getClass().getSimpleName());
            }

        }

        public void doWithField(Field field) {
            ConfigProp prop = field.getAnnotation(ConfigProp.class);
            if (prop == null)
                return;
            String s = getProp(prop);
            if (s != null && s.length() > 0) {
                Class<?> t = field.getType();
                try {
                    field.setAccessible(true);
                    field.set(target, parseValue(t, s));
                } catch (IllegalAccessException e) {
                    lctxt.error(element.getSourceLocation(), "error setting property {}.{}. value:{}",
                        target.getClass().getName(), prop.name(), s);
                }
            } else if (prop.required()) {
                lctxt.error(element.getSourceLocation(), "attribute is required:" + prop.name());
            }
        }

    }
}
